Qui génère \(\texttt{nb\_val}\) nombres aléatoires entiers entre \(\texttt{nb\_min}\) et \(\texttt{nb\_max}\)
Générer un nombre entier aléatoire
Avec randon.random, qui renvoie un réel aléatoire dans \([0; 1[\)
from random import randomfrom math import floordef random_gen_with_random (nb_min: int, nb_max: int) ->int:return floor(random() * (nb_max - nb_min) + nb_min) # floor -> arrondi inferieur (int fait la meme chose)print(random_gen_with_random(5, 10))
8
Autre version avec la fonction randint
from random import randintdef random_gen (nb_min: int, nb_max: int) ->int:return randint(nb_min, nb_max)# Note: cette fonction est un peu inutile, car elle est exactement similaire a# randint.print(random_gen(8, 18))
13
Générer une liste de nombres aléatoires
On utilise la fonction random_gen que l’on a définie plus haut.
Première version, avec une boucle :
def generation(nb_val: int, nb_min: int, nb_max: int) ->list[int]:"""Renvoie une liste de *nb_val* nombres entiers aleatoires entre nb_min, """ new_list = []for i inrange(nb_val): new_list.append(random_gen(nb_min, nb_max))return new_listprint(generation(10, 5, 10))
[7, 9, 5, 7, 8, 5, 6, 8, 9, 9]
Autre version, avec une list comprehension :
def generation_comprehension(nb_val: int, nb_min: int, nb_max: int) ->list[int]:return [random_gen(nb_min, nb_max) for i inrange(nb_val)]print(generation_comprehension(10, 5, 10))
[6, 8, 5, 8, 6, 5, 9, 9, 6, 10]
2. Vitesse d’exécution
On veut mesurer (et comparer) le temps d’exécution des fonctions de génération de nombres aléatoires créées précédemment.
On veut en fait comparer les temps de génération pour des listes contenant entre 10 et 1000 éléments (avec plusieurs valeurs intermédiaires).
Pour cela, on utilise la fonction time.time(), du module time
Fonctionnement de la fonction time
La fonction time renvoie le nombre de secondes depuis le début de “l’époque” (Epoch en anglais), c’est-à-dire depuis la “date initiale” définie par votre système d’exploitation. Sur les systèmes UNIX et leurs dérivés, cette date est généralement fixée au 1\(^{\text{er}}\) janvier 1970.
Ce qui est à comprendre, c’est que c’est un nombre qui augmente de 1 chaque seconde (les chiffres après la virgule augmentent continuellement pour avoir une mesure plus précise).
Donc, pour mesurer la durée d’exécution d’une fonction, il suffit de mémoriser dans une variable le résultat de time avant l’exécution, puis celui après, et de faire la différence entre ces deux nombres. On obtient ainsi la durée de l’exécution en secondes.
Temps d’exécution de la première méthode de génération
Voici donc le code :
from time import time# liste des nombres d'éléments dans la liste que l'on veut tester# on peut aussi utiliser range(10, 10000, 10) par exempleLIST_NUMBER_OF_ELEMENTS = [10, 100, 1000, 10000]for number_of_elements in LIST_NUMBER_OF_ELEMENTS:# on stocke le moment de début de la génération start = time()# on génère des nombres aléatoires foo = generation(number_of_elements, 42, 73)# on stocke le moment de fin de la génération end = time()# la durée d'exécution est la différence entre le moment de début et de fin# Attention : si on inverse end et start, on obtient un nombre négatif duration = end - start# on arrondi la durée, pour que le tout soit plus lisible duration =round(duration, 5)# affichage du résultatprint(f"générer {number_of_elements} à mis {duration} secondes")
générer 10 à mis 1e-05 secondes
générer 100 à mis 5e-05 secondes
générer 1000 à mis 0.00044 secondes
générer 10000 à mis 0.00466 secondes
Temps d’exécution avec des list comprehension
On utilise exactement le même code, mais avec la fonction generation_comprehension au lieu de generation :
LIST_NUMBER_OF_ELEMENTS = [10, 100, 1000, 10000]for number_of_elements in LIST_NUMBER_OF_ELEMENTS: start = time() foo = generation_comprehension(number_of_elements, 42, 73) end = time() duration = end - start duration =round(duration, 5)print(f"générer {number_of_elements} à mis {duration} secondes")
générer 10 à mis 2e-05 secondes
générer 100 à mis 5e-05 secondes
générer 1000 à mis 0.00044 secondes
générer 10000 à mis 0.00434 secondes
On remarque que le code avec des list comprehension est effectivement plus rapide.
Pour aller plus loin
Pour aller plus loin
On peut, par exemple, définir une fonction qui mesure le temps d’exécution d’une autre fonction.
Pour cela, il faut que cette nouvelle fonction (appelons-la temps_execution), prenne en argument la fonction dont on mesure le temps d’exécution.
On obtient donc quelque chose comme ça :
def temps_execution(fonction_a_tester, number_of_elements: int) ->float:# ici, on mesure le temps d'exécution start = time() foo = fonction_a_tester(number_of_elements, 42, 73) end = time()# l'idéal est de retourner le temps d'exécution plutôt# que de mettre un print à l'intérieur d'une fonction# (ce qui est à # éviter en général)return end - startprint(temps_execution(generation, 1000))print(temps_execution(generation_comprehension, 1000))
0.00047397613525390625
0.0004737377166748047
3. Inverser une liste
On veut écrire une fonction qui inverse l’ordre des éléments d’une liste
On note que, puisqu’en python, les listes ne sont pas modifiables, on devra nécessairement créer une nouvelle liste.
Une première solution fonctionne
Avec la méthode reverse
def reverse_list (liste: list) ->list:# retourne la liste (change le contenu de la variable) liste.reverse()return listeprint(reverse_list([1, 2, 3, 4, 5]))
[5, 4, 3, 2, 1]
Avec la fonction reversed
def reverse_list_reversed(liste: list) ->list:# on est obligé de mettre la fonction list pour que le# résultat soit bien une liste (voir le "pour aller plus# loin")returnlist(reversed(liste))
Pour aller plus loin - Comprendre la méthode reverse
Si on exécute ce code :
l = [1, 2, 3, 5, 8, 13, 21]r =reversed(l)print(r)
<list_reverseiterator object at 0x10736bc70>
On remarque que r n’est pas une liste, mais un itérateur.
Un itérateur est un objet que l’on parcourt (tous les itérateurs peuvent donc être mis dans une boucle for).
Le concept d’itérateur est très utile lorsque l’on crée soi-même un objet qui doit être parcouru, car python permet de créer assez facilement ses propres itérateurs.
Avec des slice
En python, on peut indexer des listes de façon assez riche. Cela s’appelle des slices (des parts en anglais, car on prend des “parts” de la liste).
def reverse_list_slice (liste: list) ->list:# :: car on prends toute la liste# -1 car on a un pas de -1 (donc on recule dans la liste)return liste[::-1]print(reverse_list_slice([1, 2, 3, 4, 5]))
[5, 4, 3, 2, 1]
Avec la méthode pop
La méthode pop des listes permet de retirer le dernier élément d’une liste. Elle retourne l’élément qu’elle retire, ce qui permet d’utiliser cet élément dans une autre fonction
Si on répète l’opération de mettre le dernier élément de l’ancienne liste à la fin de la nouvelle, retourne bien la liste
La méthode list.insert permet d’insérer un élément dans une liste, avant l’élément à l’indice précisé.
Dans ce cas, on insère avant l’indice 0, donc au début de la liste. C’est pourquoi on utilise plus pop(), mais pop(0), qui va retirer le premier élément au lieu du dernier.
On cherche à écrire une fonction qui, à partir d’une liste, sélectionne un élément sur n dans cette liste.
Par exemple, si \(n=3\), on veut transformer cette liste : \([\underline{3}, 9, 2, \underline{1}, 7, 8, \underline{4}, 3, 0, \underline{1}, 9, 7, \underline{5}, 3, 1, \underline{9}]\) en celle-ci : \([3, 1, 4, 1, 5, 9]\)
Voici la liste de test que nous allons utiliser pour la suite :
Avec une boucle et une nouvelle liste
On peut utiliser une approche classique : créer la nouvelle liste au fur-et-à-mesure, en parcourant la liste de départ.
Avec une condition sur les indices
def un_sur_n_indices(n: int, liste: list) ->list:"""Sélectionne un élément sur `n` dans `liste`""" new_list = []for i inrange(len(liste)):# si i est divisible par n (une fois sur n)if0== i % n:# on ajoute l'élément à l'indice actuel dans la# nouvelle liste new_list.append(liste[i])return new_listprint(un_sur_n_indices(3, [2, 7, 1, 8, 2, 8, 1, 8]))
[2, 8, 1]
En utilisant un pas sur range
Une technique plus simple (et plus efficace) est, plutôt que de tester pour tous les indices, d’utiliser un range dans lequel on met un pas de n.
Cela permet de n’avoir dans la boucle que les indices qui nous intéressent.
def un_sur_n_range(n: int, liste: list) ->list: new_list = []# on met un 0 pour que n soit bien le 3ème argumentfor i inrange(0, len(liste), n): new_list.append(liste[i])return new_listprint(un_sur_n_range(3, [2, 7, 1, 8, 2, 8, 1, 8]))
[2, 8, 1]
Avec une list comprehension
Pour être encore plus efficace, on peut simplement utiliser un list comprehension, en conjonction avec les techniques citées plus haut.
Le code est en fait équivalent, mais permet de créer la liste de façon plus efficace.
Avec une condition sur les indices
def un_sur_n_comprehension_indices(n: int, liste: list) ->list:return [liste[i] for i inrange(len(liste)) if0== i%n]print(un_sur_n_comprehension_indices(3, [2, 7, 1, 8, 2, 8, 1, 8]))
[2, 8, 1]
Retours à la ligne pour plus de clarté
Pour rendre le code plus clair, on peut mettre un retour à la ligne avant le for et le if :
def un_sur_n_comprehension_indices(n: int, liste: list) ->list:return [liste[i]for i inrange(len(liste))if0== i%n]
Cela est très utile quand on construit des expressions complexes, par exemple avec des list comprehension à l’intérieur de list comprehension.
En utilisant un pas sur range
On peut à nouveau utiliser un pas sur le range pour ne pas avoir à tester toutes les itérations.
def un_sur_n_comprehension_range(n: int, liste: list) ->list:return [liste[i] for i inrange(0, len(liste), n)]print(un_sur_n_comprehension_range(3, [2, 7, 1, 8, 2, 8, 1, 8]))
[2, 8, 1]
5. Maximum de nombes dans deux tableaux
On veut écrire une fonction maxDes2(tab1, tab2) qui prend en argument deux tableaux de nombres tab1 et tab2 de même longueur et retourne un tableau formé des valeurs maximales observées pour chaque indice entre les tableaux tab1 et tab2. Par exemple, maxDes2([1, 4, 5], [2, 2, 3]) retourne le tableau [2, 4, 5].
En parcourant les indices des deux tableaux
L’approche classique est de parcourir les indices i des deux tableaux (que l’on suppose de même taille), et de calculer le maximum pour chaque indice, que l’on mettra dans une nouvelle liste.
def max_des_2_indices(tab1: list[int], tab2: list[int]) ->list[int]: new_list: list[int] = []# on parcoure les indices des deux tableaux en même# temps avec ifor i inrange(len(tab1)):# la fonction max calcul le maximum de ses arguments# ici, les arguments sont les valeurs des deux# tableaux pour un même indice i new_list.append(max(tab1[i], tab2[i]))return new_listprint(max_des_2_indices([1, 4, 5], [2, 2, 3]))
[2, 4, 5]
En utilisant la fonction zip
La fonction zip va permettre de regrouper les éléments exactement comme on le souhaite. En effet, si on essaie de l’appliquer :
z =zip([3, 1, 4, 1, 5, 9, 2, 6], [2, 7, 1, 8, 2, 8, 1, 8])print(list(z)) # on utilise list pour que le contenu soit bien affiché
On observe que le résultat contient les paires d’éléments dont on veut faire le maximum : les deux premiers de chaque tableau, plus les deux deuxièmes, les deux troisièmes etc…
On peut alors proposer la solution suivante :
def max_des_2_zip(tab1: list[int], tab2: list[int]) ->list[int]: new_list: list[int] = []for couple inzip(tab1, tab2):# on note que la fonction `max` peut s'appliquer sur# une liste d'élément (ici `couple`) new_list.append(max(couple))return new_listprint(max_des_2_zip([1, 4, 5], [2, 2, 3]))
[2, 4, 5]
Avec zip et map
On remarque dans cet exercice une structure que l’on a déjà vue dans les exercices suivants : on veut appliquer une fonction particulière sur chaque élément d’une liste, puis récupérer le résultat.
L’approche classique consiste à parcourir la liste, et à créer au fur-et-à-mesure une nouvelle liste.
Cependant, une des fonctions de base de python, la fonction map, permet directement d’appliquer une fonction sur tous les éléments d’une liste, et de récupérer le résultat.
On peut donc tout simplement écrire :
def max_des_2_map(tab1: list[int], tab2: list[int]) ->list[int]:# on utilise list pour bien récupérer une liste# on applique la fonction max sur le résultat du zipreturnlist(map(max, zip(tab1, tab2)) )print(max_des_2_map([1, 4, 5], [2, 2, 3]))
[2, 4, 5]
Cette approche est une approche fonctionnelle du problème, puisque la solution est créée en composant des fonctions existantes (map, max, zip…) et sans structures de contrôles comme des boucles ou des conditions.
6. Reprogrammer la fonction zip
On veut écrire une fonction myzip(tab1, tab2) qui retourne une liste dont chaque élément d’indice i est lui-même une liste possédant deux valeurs issues des listes tab1 et tab2 à l’indice i. Par exemple, myzip({1, 4, 5}, {2, 2, 3}) retourne la liste {{1, 2}, {4, 2}, {5, 3}} ; comparer votre solution à la fonction zip() de Python.
En parcourant les indices des deux listes
def myzip_indices(tab1: list[int], tab2: list[int]) ->list[int]: zipped_list: list[int] = []for idx inrange(len(tab1)):# on ajoute le couple (tab1[idx], tab2[idx]) à la# liste de résultat. On a bien un couple d'éléments# aux mêmes indices zipped_list.append((tab1[idx], tab2[idx]))return zipped_listprint(myzip_indices([1, 4, 5], [2, 2, 3]))
[(1, 2), (4, 2), (5, 3)]
Si les deux listes ne font pas la même taille
Si les deux listes ne font pas la même taille, il faut s’arrêter quand la première liste est arrivée au bout. On peut donc simplement parcourir les indices de 1 à min(len(tab1), len(tab2)).
def myzip_indices(tab1: list[int], tab2: list[int]) ->list[int]: zipped_list: list[int] = []for idx inrange(min(len(tab1), len(tab2))):# on ajoute le couple (tab1[idx], tab2[idx]) à la# liste de résultat. On a bien un couple d'éléments# aux mêmes indices zipped_list.append((tab1[idx], tab2[idx]))return zipped_listprint(myzip_indices([1, 4, 5], [2, 2, 3, 99, 0]))print(myzip_indices([1, 4, 5, 7, 13, 4], [2, 2, 3, 99]))
On veut écrire une fonction genMat(row, col, mini, maxi) qui construit une liste de liste contenant row lignes et col colonnes et dont les valeurs sont comprises entre mini et maxi
Avec des list comprehension
def genMat(row: int, col: int, mini: int, maxi: int) ->list[list[int]]:"""Initialiser une matrice aléatoire de taille (row, col), avec des valeurs dans [mini, maxi]. """return [[randint(mini, maxi) for i inrange(col)] for j inrange(row)]print(genMat(3, 3, 0, 10))
[[5, 5, 7], [10, 6, 2], [4, 7, 1]]
En créant la liste au fur-et-à-mesure
Les list comprehension sont plus rapides, plus courtes et beaucoup plus simples à utiliser. Cet exemple est simplement là pour montrer d’autres techniques de programmation.
def genMat(row: int, col: int, mini: int, maxi: int) ->list[list[int]]:"""Initialiser une matrice aléatoire de taille (row, col), avec des valeurs dans [mini, maxi]. """ mat = []for i inrange(row): line = []for j inrange(col): line.append(randint(mini, maxi)) mat.append(line)return matprint(genMat(3, 3, 0, 10))
[[1, 1, 3], [10, 9, 2], [5, 9, 5]]
8. Diagonale d’une matrice
On veut écrire une fonction diagonale(mat) qui prend en argument une matrice de réels mat et retourne une liste contenant les éléments de sa diagonale.
M = [[1, 6, 1], [8, 0, 3], [9, 8, 8]]
Avec des list comprehension
def diagonale(mat: list[list[float]]) ->list[float]:"""Diagonale d'une matrice. Args: mat (list[list[float]]): Une matrice qui doit être carrée (sinon la diagonale n'existe pas). Returns: list[float]: La liste des coefficients diagonaux de mat. """return [mat[i][i] for i inrange(len(mat))]print(diagonale(M))
[1, 0, 8]
Pour aller plus loin
Lever une erreur si la matrice n’est pas carrée
Pour bien faire, il faudrait lever une erreur si la matrice n’est pas carrée. Pour cela, on utilise le mot clef raise, ainsi qu’une erreur classique de python. Ici, on utilisera ValueError (on pourrait également créer une classe d’erreurs nous-même, puisque les erreurs sont simplement des objets particuliers).
def diagonale(mat: list[list[float]]) ->list[float]:"""Diagonale d'une matrice. Args: mat (list[list[float]]): Une matrice qui doit être carrée (sinon la diagonale n'existe pas). Returns: list[float]: La liste des coefficients diagonaux de mat. Raises: ValueError: Si la matrice donnêé en entrée n'est pas carrée. """# si la matrice n'est pas carréeifnotall(len(mat) ==len(ligne) for ligne in mat):# on lève une exception.raiseValueError("La matrice n'est pas carrée.")return [mat[i][i] for i inrange(len(mat))]print(diagonale(M))
[1, 0, 8]
9. Trace d’une matrice
On veut écrire une fonction trace(mat) qui prend en argument une matrice de réels mat et retourne la somme de ses éléments diagonaux.
M = [[0, 1, 1], [2, 3, 5], [8, 1, 3]]
Avec une list comprehension et la fonction sum
def trace(mat: list[list[float]]) ->float:returnsum([mat[i][i] for i inrange(len(mat))])print(trace(M))
6
En réutilisant la fonction diagonale
Comme on a déjà programmé la fonction diagonale, on peut l’utiliser, car la trace d’une matrice est la somme de ses coefficients diagonaux.
Cela rend le code moins redondant et plus clair. C’est l’intérêt d’utiliser des fonctions.
10. Somme de deux matrices
On veut écrire une fonction somme(mat1, mat2) qui prend en argument deux matrices de réels mat1 et mat2, et retourne la matrice somme de ces deux matrices.
def somme(mat1: list[list[float]], mat2: list[list[float]]) ->list[list[float]]:"""Somme de deux matrices que l'on suppose de même taille. """# nombre de lignes et de colonnes de mat1 (on la prends comme référence) rows =len(mat1) cols =len(mat1[0])return [[mat1[i][j] + mat2[i][j] for j inrange(cols)] for i inrange(rows)]print(somme(A, B))
[[0, 2, 2, 4], [1, 1, 3, 3], [2, 5, 6, 9]]
Avec des zip et des map
Cette solution est plus complexe, mais elle peut avoir des avantages.
Par exemple, si on retire les fonctions list du code, la fonction va retourner un objet map, qui est une structure paresseuse (“lazy”). Cela veut dire qu’un élément donné ne sera calculé que lorsque l’on en aura besoin (lorsque l’on parcourra la matrice, par exemple).
Ce mécanisme est utile si, quand une fonction est longue à calculer, vous ne voulez pas être obligé d’attendre que toutes les valeurs soient passées par cette fonction avant de pouvoir passer à l’étape suivante : la fonction ne sera exécutée que sur les valeurs nécessaires, au fur-et-à-mesure.
def somme(mat1: list[list[float]], mat2: list[list[float]]) ->list[list[float]]:"""Somme de deux matrices que l'on suppose de même taille. """returnlist(map(lambda x: list(map(sum, zip(*x))), zip(mat1, mat2)))print(somme(A, B))
[[0, 2, 2, 4], [1, 1, 3, 3], [2, 5, 6, 9]]
11. Produit matriciel
On veut écrire une fonction produit(mat1, mat2) qui prend en argument deux matrices de réels mat1 de dimension \(n\times k\) et mat2 de dimension \(k \times m\), et retourne la matrice produit de ces deux matrices de dimension \(n \times m\).
Rappel de la formule
Soient \(mat_1 \in \mathcal{M}_{n,k}(\mathbb{R})\) et \(mat_2 \in \mathcal{M}_{k, m}(\mathbb{R})\) deux matrices.
On sait que le produit \(mat_1 \times mat_2\) est une matrice de taille \(n \times m\).
En utilisant presque directement la formule de définition du produit de matrices, on obtient cette fonction :
def produit(mat1: list[list[float]], mat2: list[list[float]]) ->list[list[float]]:"""Produit matriciel mat1 * mat2. On suppose que les matrices sont de la bonne taille, c'est-à-dire que la largeur de mat1 est égale à la longueur de mat2. """# largeur et hauteur de la matrice résultat width =len(mat1) height =len(mat2[0]) common_length =len(mat2)# on applique la formule :return [[sum(mat1[j][l] * mat2[l][i] for l inrange(common_length)) for i inrange(height)] for j inrange(width)]print(produit(A, B))
Le module numpy (qui n’est pas un module standard, il faudra donc l’installer avec pip3 --install numpy) possède des fonctions pour l’algèbre linéaire et pour les tableaux en général.
Un objet matrix est implémenté, et il permet de faire des multiplications de matrices… Avec l’opérateur * ! (Attention : si on utilise l’objet array de numpy plutôt que l’objet matrix, la multiplication sera une multiplication élément-par-élément plutôt qu’une vraie multiplication matricielle).
import numpy as npmA = np.matrix(A)mB = np.matrix(B)print(mA * mB)
[[ 2 6 5 6]
[ 9 8 8 8]
[22 19 9 19]
[ 0 0 0 0]]
Note : Avec cette méthode, le résultat n’est pas une liste de listes, mais une matrice.
12. Divisibilité par récursion
On veut écrire une fonction récursiveestDivisible(n, m) qui retourne True si et seulement si mdivisen, et false sinon.
La fonction ne doit pas utiliser les opérateurs de division, /, ou le modulo, %.
Récursion simple
On va simplement utiliser cette propriété : n divise m si et seulement si n divise m - n : \(\forall (m, n) \in \mathbb{Z}^{2}, \quad n \mid n \iff n \mid m-n\)
On utilise aussi le fait que n divise m si et seulement si la valeur absolue de n divise la valeur absolue de m.
def palindrome(tab: list) ->bool:# les listes de longueur 0 ou 1 sont toutes des palindrômesiflen(tab) <=1:returnTrue# si les deux extrémités sont différentes, ce n'est pas un palindromeif tab[0] != tab[-1]:returnFalse# récursion en enlevant les deux extrémités, que l'on a déjà vérifiéesreturn palindrome(tab[1:-1])print(estDivisible(3, 6)) # Trueprint(estDivisible(-3, 12)) # Trueprint(estDivisible(-3, 11)) # False
True
True
False
14. Nombre de chiffres par récursion
Récursion classique
def longueur(n: int) ->int: n =abs(n)if n <10:return1return1+ longueur(n /10)print(longueur(314159265358))print(longueur(73))print(longueur(1732))
12
2
4
Récursion terminale
Détails sur la récursion terminale
La récursion terminale est un récursion dans laquelle la dernière opération est l’appel récursif. Cela veut dire que le return qui contient l’appel récursif ne contient pas d’autre opération.
Par exemple, la définition précédente de longueur n’est pas terminale, car on doit ajouter 1 après l’appel récursif (la ligne de l’appel récursif est return 1 + longueur(n / 10)).
La récursion terminale à plusieurs avantages :
dans certains langages, elle est optimisée (l’optimisation de pile d’appel) et rend l’exécution plus rapide et moins coûteuse en mémoire
Elle peut être très facilement convertie en une boucle (la variable de boucle est l’accumulateur de la récursion terminale)
Dans certains livres, comme SICP, on voit que la récursion terminale est appelée “itérative”
Ici, on a aussi optimisé le programme en travaillant uniquement sur des entiers, ce qui permet d’éviter des calculs de division de flottants, qui sont inutiles.
def longueur(n: int, acc: int=0) ->int: n =int(abs(n))if n <10:return acc +1return longueur(n //10, acc +1)print(longueur(314159265358))print(longueur(73))print(longueur(1732))
12
2
4
15. Chiffres \(<4\) par récursion
On veut écrire une fonction récursivecombienInf4(n), qui prend en argument un entier naturel n, et retourne le nombre de chiffres qui le compose et qui sont strictement inférieurs à 4.
Méthode : On va simplement utiliser les opérateurs // et %, qui donnent respectivement le quotient et le reste d’une division euclidienne. En prenant en boucle le reste de la division par 10, on obtient chaque chiffre du nombre de départ.
Récursion classique
def combienInf4(n: int) ->int:# si le dernier chiffre de n est un 4if n %10<4:# si n est un chiffreif n <10:# le résultat est 1return1# on ajoute 1 à la récursion car n finit par 4return1+ combienInf4(n //10)# si n ne finit pas par 4 :# si n est un chiffreif n <10:# aucun 4 dans nreturn0# on ajoute rien à la récursion car n ne finit pas par 4return combienInf4(n //10)print(combienInf4(123456))print(combienInf4(314159265358))print(combienInf4(789456))
3
5
0
Avec des conversions de types
Pour rendre le code plus simple (et plus lisible pour un programmeur averti), on utilise le fait que la fonction int puisse convertir des booléens en entiers.
def combienInf4(n: int) ->int:if n <10:# (n < 4) est un booléen# int(True) vaut 1, et int(False) vaut 0returnint(n <4)# on ajoute 1 à la récursion si le dernier chiffre de n est 4returnint(n %10<4) + combienInf4(n //10)print(combienInf4(123456))print(combienInf4(314159265358))print(combienInf4(789456))